package com.bestattempt.utility.bind;

import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.lang.reflect.*;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import static com.bestattempt.utility.reflection.ReflectionUtil.*;
import com.bestattempt.utility.reflection.ReflectionUtil;
import com.bestattempt.utility.bind.initializers.hibernateImpl.HibernateListInitializer;
import com.bestattempt.utility.bind.initializers.hibernateImpl.HibernateMapInitializer;

/**
 * TODO: Document me
 *
 * @author Ganeshji Marwaha
 * @since Oct 13, 2006
 */
public class PojoInitializer {
    protected Log log = LogFactory.getLog(PojoInitializer.class);
    private static PojoInitializer initializer = new PojoInitializer();

    /**
     * <p>Convenience method to initialize a given class depending on the parameters available in the request.
     * This method introduces a dependency with servlet specification (such as servlet-api.jar), but the advantage
     * is that the caller need not by himself create a request parameter map before calling this method.</p>
     *
     * @see <code>initialize(List<String>, Class)</code>
     *
     * @param request - HttpServletRequest, whose parameters are the driving force behind the initialization process.
     * @param clazz - The class of the object that needs to be initialized based on the <code>request</code>.
     * @return fully initialized clazz instance
     */
    public static <O> O initialize(HttpServletRequest request, Class<O> clazz) {
        return initializer.initialize(new ArrayList<String>(request.getParameterMap().keySet()), clazz);
    }

    /**
     * <p>Convenience method to initialize a given object depending on the parameters available in the request.
     * This method introduces a dependency with servlet specification (such as servlet-api.jar), but the advantage
     * is that the caller need not by himself create a request parameter map before calling this method.</p>
     * <p>For more information</p>
     *
     * @see <code>initialize(List<String>, O)</code>
     *
     * @param request
     * @param object - obj to be initialized (holder object also sometimes referred to as command in spring parlance)
     * @return fully initialized obj
     */
    public static <O> O initialize(HttpServletRequest request, O object) {
        return initializer.initialize(new ArrayList<String>(request.getParameterMap().keySet()), object);
    }

    /**
     * <p>This method works by instantiating the passed class, then instantiating the required aggregates based
     * on the incoming request parameters.</p>
     *
     * <p> Examples of some supported parameter types are </p>
     * <ul>
     * <li>user.firstName - This instantiates user attribute of the command (holder) class </li>
     * <li>user.address.city - This instantiates user attribute, then instantiates the address attribute of user</li>
     * <li>user.addresses[0].city - This instantiates user attribute, then instantiates the addresses collection.
     * Supported collection types are List and Map at present. This is called indexed parameters. If the collection is
     * a list, the numeric indexes like [0] are supported. If the collection is a Map, then string indexes like ['one']
     * are supported. This is under the assumption, that this is the level of request mapping that would be common.
     * Then, the generic type of the collection is identified and instantiated. Eg: List<Address> will instantiate
     * Address.class to be populated into the index of the collection. Then the instantiated object is populated into
     * the collection type. </li>
     * </ul>
     *
     * @see <code>initialize(List<String>, O)</code>
     *
     * @param paramNames
     * @param clazz
     * @return populated bean
     */
    public <O> O initialize(List<String> paramNames, Class<O> clazz) {
        O o = newInstance(clazz);
        return initialize(paramNames, o);
    }

    /**
     * <p>This method works by then instantiating the required aggregates of the passed in object instance based
     * on the incoming request parameters.</p>
     *
     * <p>We first start by cleaning up the list of paramNames. Eg: user.firstName, user.lastName, user.xyz all finally
     * boil down to instantiate user object only. So, a cleanup will ensure a performance gain plus cleaner code.
     * @see <code>clean(List<String>)</p>
     *
     * <p>Each paramName is checked to find if it is non-indexed or indexed.
     *
     * If it is indexed, then the <code> initIndexed(String, Object)<code> method is called to perform
     * its initialization.
     *
     * If it is non-indexed, then the <code> initObject(String, Object)</code> is called to perform its initialization.
     *
     * If it is neither (because it is a nested path), then depth of then nested path is navigated and for each path
     * expression in the nested path <code>initIndexed</code> or <code>initObject</code> is called.</p>
     *
     * @see <code>initialize(List<String>, Class<O>)</code>
     *
     * @param paramNames
     * @param object
     * @return populated bean
     */
    public <O> O initialize(List<String> paramNames, O object) {
        List<String> params = clean(paramNames);
        for(String param: params) {
            try {
                if(StringUtils.contains(param, ".")) {
                    initNested(param, object);
                } else if( StringUtils.contains(param, "[") ) {
                    initIndexed(param, object);
                } else {
                    initObject(param, object);
                }
            } catch(InitializerException e) {
                log.warn("Path did not evaluate. Param: " + param + ". Proceeding with the next parameter.");
                log.warn("Root Cause: " + e.getMessage());
            }
        }
        return object;
    }

    /**
     *
     * @param param
     * @param object
     * @return object
     */
    private Object initNested(String param, Object object) {
        String[] parts = param.split("\\.");
        for(String part: parts) {
            object = part.contains("[") ? initIndexed(part, object) : initObject(part, object);
        }
        return object;
    }

    /**
     * <p>The field of the object that needs to be initialized is taken from the param. Depending on the type of the
     * field, the appropriate initializer for that field is identified. Eg: ListInitializer if the field type is List.
     * The identified initializer is then delegated the task of initilizing that particular aggregate</p>
     *
     * @param param
     * @param object
     * @return populated bean
     */
    private Object initIndexed(String param, Object object) {
        String part = param.substring(0, param.indexOf("["));
        String indexStr = param.substring(param.indexOf("[")+1, param.lastIndexOf("]"));

        Field field = ReflectionUtil.getField(object, part, true);
        return Initializers.get(field.getType()).initialize(object, field, indexStr);
    }

    /**
     * <p>The field of the object that needs to be initialized is taken from the param. Depending on the type of the
     * field, the appropriate initializer for that field is identified. Eg: ObjectInitializer for all non-collection
     * objects. The identified initializer is then delegated the task of initilizing that particular aggregate</p>
     * @param param
     * @param object
     * @return populated bean
     */
    private Object initObject(String param, Object object) {
        Field field = ReflectionUtil.getField(object, param, true);
        return Initializers.get(field.getType()).initialize(object, field);
    }

    /**
     * <ul>
     * <li> List of parameters is first sorted</li>
     * <li> Then duplicates are removed </li>
     * <li> Then unnecessary parameters (though not duplicates) are removed</li>
     * </ul>
     * <p> This essentially reduces the parameter list to the minimum required for initalization purposes </p>
     * @param paramNames
     * @return cleaned list of parameters
     */
    private List<String> clean(List<String> paramNames) {
        SortedSet<String> cleaned = new TreeSet<String>();
        for(String param: paramNames) {
            if(StringUtils.contains(param, ".")) {
                int lastIndex = param.lastIndexOf(".");
                cleaned.add(new StringBuilder(param).replace(lastIndex, param.length(), "").toString());
            }
        }
        return new ArrayList<String>(cleaned);
    }

    /**
     *
     */
    public enum OrmSupport {
        HIBERNATE, TOPLINK, KODO
    }

    /**
     * 
     * @param ormSupport
     */
    public void setOrmSupport(OrmSupport ormSupport) {
        switch(ormSupport) {
            case HIBERNATE:
                Initializers.registerInitializer(List.class, new HibernateListInitializer());
                Initializers.registerInitializer(Map.class, new HibernateMapInitializer());
                break;
            case TOPLINK:
                throw new UnsupportedOperationException("Toplink Orm not yet supported");
            case KODO:
                throw new UnsupportedOperationException("KODO Orm not yet supported");
        }
    }
}
